//----------------------------------------------------------------------------//
//                                                                            //
// ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation  //
// and distributed under the MIT License (MIT).                               //
//                                                                            //
// Copyright (c) Guillaume Blanc                                              //
//                                                                            //
// Permission is hereby granted, free of charge, to any person obtaining a    //
// copy of this software and associated documentation files (the "Software"), //
// to deal in the Software without restriction, including without limitation  //
// the rights to use, copy, modify, merge, publish, distribute, sublicense,   //
// and/or sell copies of the Software, and to permit persons to whom the      //
// Software is furnished to do so, subject to the following conditions:       //
//                                                                            //
// The above copyright notice and this permission notice shall be included in //
// all copies or substantial portions of the Software.                        //
//                                                                            //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   //
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    //
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    //
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        //
// DEALINGS IN THE SOFTWARE.                                                  //
//                                                                            //
//----------------------------------------------------------------------------//

#include "SkrAnim/ozz/options/options.h"

#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <new>
#include <sstream>

namespace ozz
{
namespace options
{

namespace internal
{

namespace
{
// Declares and instantiates a global object that aims to delete the global
// BaseRegistrer parser. This global BaseRegistrer parser is allocated when
// options are being registered or when ParseCommandLine is called.
static class GlobalRegistrer
{
public:
    ~GlobalRegistrer()
    {
        if (parser_)
        {
            parser_->~Parser();
            parser_ = nullptr;
        }
    }

    static Parser* Construct()
    {
        if (!parser_)
        {
            parser_ = new (placeholder_) Parser;
        }
        return parser_;
    }

    static Parser* parser() { return parser_; }

private:
    // Take advantage of the default initialization (to 0) of file-scope level
    // variables to ensure that registration does not depend on the initialization
    // order of any other variable.
    static Parser* parser_;

    // The global parser is allocated from the static section in order to avoid
    // any heap allocation before entering the main (and after exiting it also).
    // Uses an array of pointers (void*) in order to ensure a native minimum
    // alignment.
    static void* placeholder_[];
} g_global_registrer;

// Default initialization to 0 only. See parser_ member document above.
Parser* GlobalRegistrer::parser_;

// Compute placeholder_ array size and performs default initialization only.
// See placeholder_ member document above.
static const size_t kParserPlaceholerSize =
    (sizeof(Parser) + sizeof(void*) - 1) / sizeof(void*);
void* GlobalRegistrer::placeholder_[kParserPlaceholerSize];
} // namespace

// Implements Registrer constructor and destructor.
template <typename _Option>
Registrer<_Option>::Registrer(const char* _name, const char* _help, typename _Option::Type _default, bool _required, typename _Option::ValidateFn _fn)
    : _Option(_name, _help, _default, _required, _fn)
{
    if (!internal::g_global_registrer.Construct()->RegisterOption(this))
    {
        std::cerr << "Failed to register option " << _name << std::endl;
    }
}

template <typename _Option>
Registrer<_Option>::~Registrer()
{
    Parser* parser = internal::g_global_registrer.parser();
    if (parser)
    {
        parser->UnregisterOption(this);
    }
}

// Explicit instantiation of all supported types of Registrer.
template class OZZ_OPTIONS_DLL Registrer<TypedOption<bool>>;
template class OZZ_OPTIONS_DLL Registrer<TypedOption<int>>;
template class OZZ_OPTIONS_DLL Registrer<TypedOption<float>>;
template class OZZ_OPTIONS_DLL Registrer<TypedOption<const char*>>;
} // namespace internal

// Construct the parser if no option is registered.
// This local parser will be deleted automatically. This allows to query the
// executable path and name, as well as the usage and version strings even if
// no option is registered.
ParseResult ParseCommandLine(int _argc, const char* const* _argv, const char* _version, const char* _usage)
{
    // Need to instantiate a local parser if no option is registered.
    Parser* parser = internal::g_global_registrer.Construct();

    // Set usage and version strings and parse command line.
    parser->set_usage(_usage);
    parser->set_version(_version);
    return parser->Parse(_argc, _argv);
}

// A nullptr parser means that no option is registered and that ParseCommandLine
// has not been called.
ozz::string ParsedExecutablePath()
{
    Parser* parser = internal::g_global_registrer.parser();
    if (!parser)
    {
        return {};
    }
    return parser->executable_path();
}

// See Parser::ExecutableName().
const char* ParsedExecutableName()
{
    Parser* parser = internal::g_global_registrer.parser();
    if (!parser)
    {
        return "";
    }
    return parser->executable_name();
}

// See Parser::Usage().
const char* ParsedExecutableUsage()
{
    Parser* parser = internal::g_global_registrer.parser();
    if (!parser)
    {
        return "";
    }
    return parser->usage();
}

namespace
{

// Portable case insensitive string comparison functions.
int StrICmp(const char* _left, const char* _right)
{
    int l, r;
    do
    {
        l = static_cast<unsigned char>(std::tolower(*(_left++)));
        r = static_cast<unsigned char>(std::tolower(*(_right++)));
    } while (l && (l == r));
    return l - r;
}

int StrNICmp(const char* _left, const char* _right, size_t _count)
{
    if (_count)
    {
        int l, r;
        do
        {
            l = static_cast<unsigned char>(std::tolower(*(_left++)));
            r = static_cast<unsigned char>(std::tolower(*(_right++)));
        } while (--_count && l && (l == r));
        return l - r;
    }
    return 0;
}

// Returns the first character after _option, or nullptr if option has not been
// found.
const char* ParseOption(const char* _argv, const char* _prefix, const char* _option)
{
    const size_t prefix_len = std::strlen(_prefix);
    const size_t option_len = std::strlen(_option);

    // All options start with --.
    if (StrNICmp(_argv, _prefix, prefix_len) != 0)
    {
        return nullptr;
    }
    _argv += prefix_len;
    if (!StrNICmp(_argv, _option, option_len))
    {
        return _argv + option_len;
    }

    return nullptr;
}

bool Parse(const char* _argv, const char* _option, bool* _value)
{
    assert(_value && _option && _argv);

    const char* option_end = ParseOption(_argv, "--", _option);
    if (!option_end)
    {
        // Test for the explicit option of form --no*.
        option_end = ParseOption(_argv, "--no", _option);
        if (option_end && *option_end == '\0')
        {
            *_value = false;
            return true;
        }
        return false;
    }

    // Option _option was found, now checks for a valid value.
    if (*option_end == '\0')
    {
        // Implicit boolean options have no trailing characters.
        *_value = true;
        return true;
    }
    else if (*option_end == '=')
    {
        // Explicit options values are set after the '=' character.
        // Using StrICmp function ensures an exact match, ie no trailing characters.
        for (++option_end; std::isspace(*option_end);
            ++option_end)
        { // Trims spaces.
        }
        const char* true_options[] = { "yes", "true", "1", "t", "y" };
        for (size_t i = 0; i < sizeof(true_options) / sizeof(true_options[0]);
            i++)
        {
            if (!StrICmp(option_end, true_options[i]))
            {
                *_value = true;
                return true;
            }
        }
        const char* false_options[] = { "no", "false", "0", "f", "n" };
        for (size_t i = 0; i < sizeof(false_options) / sizeof(false_options[0]);
            ++i)
        {
            if (!StrICmp(option_end, false_options[i]))
            {
                *_value = false;
                return true;
            }
        }
    }
    return false; // Option was not found or is invalid.
}

bool Parse(const char* _argv, const char* _option, float* _value)
{
    assert(_value && _option && _argv);

    const char* option_end = ParseOption(_argv, "--", _option);
    if (option_end && *option_end == '=')
    {
        for (++option_end; std::isspace(*option_end);
            ++option_end)
        { // Trims spaces.
        }
        char* found;
        double double_value = std::strtod(option_end, &found);
        // No trailing characters are allowed.
        if (found != option_end && *found == '\0')
        {
            *_value = static_cast<float>(double_value);
            return true;
        }
    }
    return false; // Option was not found or is invalid.
}

bool Parse(const char* _argv, const char* _option, int* _value)
{
    assert(_value && _option && _argv);

    const char* option_end = ParseOption(_argv, "--", _option);
    if (option_end && *option_end == '=')
    {
        for (++option_end; std::isspace(*option_end);
            ++option_end)
        { // Trims spaces.
        }
        char* found;
        long long_value = std::strtol(option_end, &found, 10);
        // No trailing characters are allowed. If base is 0,
        if (found != option_end && *found == '\0')
        {
            *_value = static_cast<int>(long_value);
            return true;
        }
    }
    return false; // Option was not found or is invalid.
}

bool Parse(const char* _argv, const char* _option, const char** _value)
{
    assert(_value && _option && _argv);

    const char* option_end = ParseOption(_argv, "--", _option);
    if (option_end && *option_end == '=')
    {
        for (++option_end; std::isspace(*option_end);
            ++option_end)
        { // Trims spaces.
        }
        *_value = option_end;
        return true;
    }
    return false; // Option was not found or is invalid.
}

// Format option type using template specialization.
template <typename _Type>
const char* FormatOptionType();

// Specialization of FormatOptionType for all supported types.
template <>
const char* FormatOptionType<bool>()
{
    return "bool";
}
template <>
const char* FormatOptionType<float>()
{
    return "float";
}
template <>
const char* FormatOptionType<int>()
{
    return "int";
}
template <>
const char* FormatOptionType<const char*>()
{
    return "string";
}

// Validate exclusive options.
bool ValidateExclusiveOption(const Option& _option, int _argc)
{
    if (static_cast<const BoolOption&>(_option).value() && _argc != 1)
    {
        std::cout << "Option \"" << _option.name()
                  << "\" is an exclusive option. "
                     "It must not be used with any other option."
                  << std::endl;
        // This is an exclusive flag.
        return false;
    }
    return true;
}
} // namespace

Option::Option(const char* _name, const char* _help, bool _required, ValidateFn _validate)
    : name_(_name ? _name : "")
    , help_(_help ? _help : "...")
    , required_(_required)
    , parsed_(false)
    , validate_(_validate)
{
}

Option::~Option() {}

bool Option::Validate(int _argc)
{
    if (validate_)
    {
        return (*validate_)(*this, _argc);
    }
    return true;
}

bool Option::Parse(const char* _argv)
{
    if (ParseImpl(_argv) && !parsed_)
    { // Fails if argument's already specified.
        parsed_ = true;
        return true;
    }
    return false;
}

void Option::RestoreDefault()
{
    parsed_ = false;
    RestoreDefaultImpl(); // Restores actual value.
}

template <typename _Type>
bool TypedOption<_Type>::ParseImpl(const char* _argv)
{
    return ozz::options::Parse(_argv, name(), &value_);
}

template <typename _Type>
ozz::string TypedOption<_Type>::FormatDefault() const
{
    std::stringstream str;
    str << "\"" << std::boolalpha << default_ << "\"";
    return str.str().c_str();
}

template <typename _Type>
const char* TypedOption<_Type>::FormatType() const
{
    return ozz::options::FormatOptionType<_Type>();
}

// Explicit instantiation of all supported types of TypedOption.
template class TypedOption<bool>;
template class TypedOption<int>;
template class TypedOption<float>;
template class TypedOption<const char*>;

Parser::Parser()
    : options_count_(0)
    , builtin_options_count_(0)
    , executable_path_begin_("")
    , executable_path_end_(executable_path_begin_ + 1)
    , executable_name_("")
    , version_(nullptr)
    , usage_(nullptr)
    , builtin_version_("version", "Displays application version", false, false, &ValidateExclusiveOption)
    , builtin_help_("help", "Displays help", false, false, &ValidateExclusiveOption)
{
    // Set default values.
    set_version(nullptr);
    set_usage(nullptr);
    // Registers built-in options.
    RegisterOption(&builtin_version_);
    RegisterOption(&builtin_help_);
    builtin_options_count_ = options_count_;
}

Parser::~Parser()
{
    UnregisterOption(&builtin_version_);
    UnregisterOption(&builtin_help_);
}

ParseResult Parser::Parse(int _argc, const char* const* _argv)
{
    if (_argc < 1 || !_argv)
    {
        return kExitFailure;
    }

    // Select the left most '/' or '\\' separator.
    const char* path_end =
        std::max(std::strrchr(_argv[0], '/'), strrchr(_argv[0], '\\'));
    if (path_end)
    {
        ++path_end; // Includes the last separator.
        executable_path_begin_ = _argv[0];
        executable_path_end_ = path_end;
        executable_name_ = path_end;
    }
    else
    {
        executable_path_begin_ = "";
        executable_path_end_ = executable_path_begin_ + 1;
        executable_name_ = _argv[0];
    }

    // The first argument is skipped because it is the program path.
    ++_argv;
    --_argc;

    // Hides all arguments after a "--" argument, substitutes argc to argc_trunc.
    int argc_trunc = 0;
    for (; argc_trunc < _argc && std::strcmp(_argv[argc_trunc], "--") != 0;
        ++argc_trunc)
    {
    }

    // Restores built-in options to their default value in case parsing in done
    // multiple times.
    for (int i = 0; i < options_count_; ++i)
    {
        options_[i]->RestoreDefault();
    }

    // Iterates all arguments and all options.
    ParseResult result = kSuccess;
    for (int i = 0; i < argc_trunc; ++i)
    {
        const char* argv = _argv[i];

        // Empty arguments aren't consider invalid.
        if (*argv == 0)
        {
            continue;
        }

        int j = 0;
        for (; j < options_count_; ++j)
        {
            if (options_[j]->Parse(argv))
            {
                break; // Also breaks if argument is duplicated.
            }
        }
        // An invalid (or duplicated) command line argument is a fatal failure.
        if (j == options_count_)
        {
            std::cout << "Invalid command line argument:\"" << argv << "\"."
                      << std::endl;
            result = kExitFailure;
            break;
        }
    }

    // Validate build-in options first.
    // They need to be validated and tested first, as they have priority over
    // others, even required once.
    if (!builtin_help_.Validate(argc_trunc) ||
        !builtin_version_.Validate(argc_trunc))
    {
        result = kExitFailure;
    }

    // Display built-in help.
    if (result == kSuccess && builtin_help_)
    {
        Help();
        result = kExitSuccess;
    }

    // Display built-in version.
    if (result == kSuccess && builtin_version_)
    {
        std::cout << "version " << version() << std::endl;
        result = kExitSuccess;
    }

    // Ensures all required options were specified in the command line.
    if (result == kSuccess)
    {
        for (int i = 0; i < options_count_; ++i)
        {
            if (!options_[i]->statisfied())
            {
                std::cout << "Required option \"" << options_[i]->name()
                          << "\" is not specified." << std::endl;
                result = kExitFailure;
                break;
            }
        }
    }

    // Validates all options.
    if (result == kSuccess)
    {
        for (int i = 0; i < options_count_; ++i)
        {
            if (!options_[i]->Validate(argc_trunc))
            {
                result = kExitFailure;
                break;
            }
        }
    }

    // Also displays help if an error occurred.
    if (result == kExitFailure)
    {
        Help();
    }

    return result;
}

void Parser::Help()
{
    std::cout << std::endl;
    std::cout << executable_name() << " version " << version() << std::endl;
    std::cout << usage() << std::endl;
    std::cout << std::endl;

    // Displays usage line.
    std::cout << "Usage:" << std::endl;
    std::cout << executable_name();
    for (int i = 0; i < options_count_; ++i)
    {
        const Option& option = *options_[i];
        std::cout << ' ';
        if (option.required())
        {
            std::cout << '[';
        }
        std::cout << "--" << option.name();
        if (option.required())
        {
            std::cout << ']';
        }
    }
    std::cout << std::endl;

    // Displays option details.
    std::cout << "\nWhere:" << std::endl;
    for (int i = 0; i < options_count_; ++i)
    {
        const Option& option = *options_[i];
        const std::string option_str =
            std::string(" --") + option.name() + "=<" + option.FormatType() + ">";
        std::cout << std::setiosflags(std::ios::left) << std::setw(28) << option_str
                  << std::resetiosflags(std::ios::left) << option.help()
                  << "(default is " << option.FormatDefault() << ")" << std::endl;
    }

    const char* how_to =
        "\n"
        "Syntax:\n"
        "To set an option from the command line use the form --option=value for\n"
        "non-boolean options, and --option/--nooption for booleans.\n"
        "For example, \"foo --var=46\" will set \"var\" variable to 46.\n"
        "If \"var\" type is not compatible with the specified argument (in this\n"
        "case not an integer, a float or a string), then this help message\n"
        "is displayed and application exits.\n"
        "\n"
        "Boolean options can be set using different syntax:\n"
        "- to set a boolean option to true: \"--var\", \"--var=true\", "
        "\"--var=t\","
        "  \"--var=yes\", \"--var=y\", \"--var=1\".\n"
        "- to set a boolean option to false: \"--novar\", \"--var=false\","
        "   \"--var=f\", \"--var=no\", \"--var=n\", \"--var=0\".\n"
        "Consistently using single-form --option/--nooption is recommended "
        "though.";
    std::cout << how_to << std::endl;
}

namespace
{
// Sort required options first, and then based on their names.
bool sort_options(Option* _left, Option* _right)
{
    return (_left->required() && !_right->required()) ||
        (_left->required() == _right->required() &&
            std::strcmp(_left->name(), _right->name()) < 0);
}
} // namespace

bool Parser::RegisterOption(Option* _option)
{
    if (!_option)
    {
        return false;
    }
    if (options_count_ == sizeof(options_) / sizeof(options_[0]))
    {
        return false;
    }

    // Tests for duplicate options.
    if (std::count(options_, options_end(), _option) != 0)
    {
        return false;
    }

    // Empty (or nullptr) names aren't allowed.
    if (_option->name()[0] == '\0')
    {
        std::cerr << "Empty (or nullptr) names aren't allowed." << std::endl;
        return false;
    }

    // Test for duplicate options' name.
    for (int i = 0; i < options_count_; ++i)
    {
        if (StrICmp(options_[i]->name(), _option->name()) == 0)
        {
            std::cerr << "Option name:\"" << _option->name()
                      << "\" already registered." << std::endl;
            return false;
        }
    }

    // Adds the option and maintains lexical order.
    options_[options_count_++] = _option;
    std::inplace_merge(options_, options_end() - 1, options_end(), &sort_options);

    return true;
}

bool Parser::UnregisterOption(Option* _option)
{
    if (!_option)
    {
        return false;
    }

    // Finds and removes _option from the collection.
    Option** it = std::remove(options_, options_end(), _option);
    if (it != options_end())
    {
        return --options_count_ == builtin_options_count_;
    }
    return false;
}

int Parser::max_options() const
{
    return sizeof(options_) / sizeof(options_[0]) - builtin_options_count_;
}

void Parser::set_usage(const char* _usage)
{
    usage_ = _usage ? _usage : "unspecified usage";
}

void Parser::set_version(const char* _version)
{
    version_ = _version ? _version : "unspecified version";
}

const char* Parser::usage() const { return usage_; }

const char* Parser::version() const { return version_; }

ozz::string Parser::executable_path() const
{
    return ozz::string(executable_path_begin_, executable_path_end_);
}

const char* Parser::executable_name() const { return executable_name_; }
} // namespace options
} // namespace ozz
